package com.hvgroup.modules.sanic.service.impl.chat.template;

import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.hvgroup.modules.sanic.domain.enums.DatasetIType;
import com.hvgroup.modules.sanic.domain.enums.PlatformType;
import com.hvgroup.modules.sanic.domain.po.*;
import com.hvgroup.modules.sanic.domain.qo.CPReqBody;
import com.hvgroup.modules.sanic.domain.qo.QaQuestionQuery;
import com.hvgroup.modules.sanic.repository.*;
import com.hvgroup.modules.sanic.service.impl.chat.EventData;
import com.hvgroup.modules.sanic.service.impl.chat.chat.EventHandler;
import com.hvgroup.utils.StringUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

import java.io.IOException;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Slf4j
@Component
@RequiredArgsConstructor
public class DifyExecuteTemplateImpl implements ExecuteTemplate {

    private final PlatformType type = PlatformType.DIFY;
    private final LlmModelConfigRepository llmModelConfigRepository;
    private final LlmModelConfigApiRepository llmModelConfigApiRepository;
    private final LlmAppRepository llmAppRepository;

    private final LlmDatasetsRepository llmDatasetsRepository;
    private final LlmDatasetsTpmlRepository llmDatasetsTpmlRepository;

    private final LlmRolePermissionRepository llmRolePermissionRepository;

    private final List<EventHandler> allHandlers;

    private List<EventHandler> getHandlers() {
        // 推荐懒加载或初始化时处理
        return allHandlers.stream()
                .filter(o -> type.equals(o.getType()))
                .collect(Collectors.toList());
    }

    @Override
    public PlatformType getType() {
        return type;
    }

    public WebClient buildApi(LlmModelConfig config, LlmApp app) {
        return WebClient.builder()
                .baseUrl(config.getPlatformAddr())
                .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + app.getApiKey())
                .build();
    }

    @Override
    public Mono<JsonNode> buildDsetApi(LlmModelConfig config, CPReqBody cpReqBody) {
        Map<String, Object> map = cpReqBody.getBody();
        // DatasetIType
//        String datasetIType = (String) map.get("datasetIType");

        DatasetIType iType = cpReqBody.getDatasetIType();
        Mono<LlmModelConfigApi> configApiMono = Mono.fromCallable(() ->
                llmModelConfigApiRepository.findFirstByPlatformIdAndApiGroupAndApiType(config.getId(), 2, iType.getValue())
                        .orElseThrow(() -> new RuntimeException("平台ID不存在!"))
        ).subscribeOn(Schedulers.boundedElastic());

        return configApiMono.flatMap(configApi -> {
            WebClient client = WebClient.builder()
                    .baseUrl(config.getPlatformAddr())  // 平台地址已获取
                    .build();

            switch (configApi.getApiType()) {
                case 1: {
                    // 知识库列表
                    // TODO 那么怎么同步和平台的数据? 例如不同的平台,仅有XX_ID可以定位, 通过这个ID定位?
                    return Mono.fromCallable(() -> {
                                //  运行在 boundedElastic 线程池中，避免阻塞主线程
                                List<LlmRolePermission> use_list = llmRolePermissionRepository.findByUserId(cpReqBody.getUserId(), cpReqBody.getPlatformId());

                                List<Long> dsId = use_list.stream()
                                        .flatMap(o -> Arrays.stream(StringUtils.defaultString(o.getDatasetsId(), "").split(",")))
                                        .map(String::trim)
                                        .filter(StringUtils::isNotBlank)
                                        .filter(s -> s.matches("\\d+"))
                                        .map(Long::valueOf)
                                        .collect(Collectors.toList());

                                return llmDatasetsRepository.findAllById(dsId);  // 注意，这也是同步阻塞方法
                            })
                            .subscribeOn(Schedulers.boundedElastic())
                            .flatMap(datasets -> {
                                // 你可以改造成 JsonNode 后再返回
                                ObjectNode json = new ObjectMapper().createObjectNode();
                                json.put("count", datasets.size());
                                json.set("items", new ObjectMapper().valueToTree(datasets));
                                return Mono.just(json);
                            });
                }
                case 2:
                    // 知识库文档列表
                {
                    // 权限相关处理  改查本地表
                    // TODO 那么怎么同步和平台的数据? 例如不同的平台,仅有XX_ID可以定位, 通过这个ID定位?
                    return Mono.fromCallable(() -> {
                                //  运行在 boundedElastic 线程池中，避免阻塞主线程
                                return llmDatasetsTpmlRepository.findAllByDatasetsId(Long.valueOf(cpReqBody.getLevelId_3()));  // 注意，这也是同步阻塞方法
                            })
                            .subscribeOn(Schedulers.boundedElastic())
                            .flatMap(datasets -> {
                                // 你可以改造成 JsonNode 后再返回
                                ObjectNode json = new ObjectMapper().createObjectNode();
                                json.put("count", datasets.size());
                                json.set("items", new ObjectMapper().valueToTree(datasets));
                                return Mono.just(json);
                            });
                }
                case 3:
                    // 根据文件创建文档
                case 4:
                    // 删除文档
                    return client.post()
                            .uri(uriBuilder -> {
                                // case 4(obj.apiType) 对应删除, 因此不需要额外查询
                                uriBuilder.path(configApi.getApiAdd());

                                Map<String, Object> params = (Map<String, Object>) map.get("params");
                                if (params != null) {
                                    params.forEach((k, v) -> {
                                        if (ObjectUtils.isNotEmpty(v)) {
                                            uriBuilder.queryParam(k, v);
                                        }
                                    });
                                }
                                return uriBuilder.build();
                            })
                            .header(HttpHeaders.AUTHORIZATION, "Bearer " + config.getDatasetsApiKey())
                            .accept(MediaType.TEXT_EVENT_STREAM)
                            .retrieve()
                            .bodyToMono(JsonNode.class)
                            .timeout(Duration.ofSeconds(10));
                default:
                    return Mono.error(new RuntimeException("未定义的功能, 请联系管理员!"));
            }
        });
    }

    @Override
    public Mono<String> deleteDatasetsTpml(LlmModelConfig config, CPReqBody cpReqBody) {

        Mono<LlmModelConfigApi> configApiMono = Mono.fromCallable(() ->
                llmModelConfigApiRepository.findFirstByPlatformIdAndApiGroupAndApiType(
                                config.getId(), 2, DatasetIType.DELETE_DOCUMENTS.getValue())
                        .orElseThrow(() -> new RuntimeException("平台ID不存在!"))
        ).subscribeOn(Schedulers.boundedElastic());

        // 异步查找模板
        Mono<LlmDatasetsTpml> tpmlMono = Mono.fromCallable(() ->
                llmDatasetsTpmlRepository.findById(Long.valueOf(cpReqBody.getLevelId_3()))
                        .orElseThrow(() -> new RuntimeException("模板不存在!"))
        ).subscribeOn(Schedulers.boundedElastic());


        return Mono.zip(configApiMono, tpmlMono)
                .flatMap(tuple -> {
                    LlmModelConfigApi configApi = tuple.getT1();
                    LlmDatasetsTpml ldt = tuple.getT2();

                    WebClient client = WebClient.builder()
                            .baseUrl(config.getPlatformAddr())
                            .build();
                    // 构造请求路径
                    String uri = configApi.getApiAdd()
                            .replace("{dataset_id}", ldt.getDatasetsId())
                            .replace("{document_id}", ldt.getDocId());

                    return client.delete()
                            .uri(uri)
                            .header(HttpHeaders.AUTHORIZATION, "Bearer " + config.getDatasetsApiKey())
                            .accept(MediaType.TEXT_EVENT_STREAM)
                            .retrieve()
                            .bodyToMono(String.class)
                            .timeout(Duration.ofSeconds(10))
                            .publishOn(Schedulers.boundedElastic())
                            .doOnSuccess(resp -> {
                                llmDatasetsTpmlRepository.delete(ldt);
                            });
                });
    }

    @Override
    public Object uploadDatasetsTpml(LlmModelConfig config, CPReqBody cpReqBody) {
        // 查询数据集（阻塞）
        LlmDataset llmDataset = llmDatasetsRepository.findById(Long.valueOf(cpReqBody.getLevelId_2()))
                .orElseThrow(() -> new RuntimeException("知识库ID不存在!"));

        // 查询配置 API（阻塞）
        LlmModelConfigApi configApi = llmModelConfigApiRepository.findFirstByPlatformIdAndApiGroupAndApiType(
                        config.getId(), 2, DatasetIType.CREATE_DOCUMENT_BY_FILE.getValue())
                .orElseThrow(() -> new RuntimeException("平台ID不存在!"));

        MultipartFile file = cpReqBody.getFile();

        // 构建 WebClient
        WebClient client = WebClient.builder()
                .baseUrl(config.getPlatformAddr())
                .build();

        // 构造 URI
        String uri = configApi.getApiAdd()
                .replace("${dataset_id}", llmDataset.getDatasetsId());

        // 构造 JSON 字符串
        String jsonData = "{\"indexing_technique\":\"high_quality\",\"process_rule\":{\"rules\":{\"pre_processing_rules\":[{\"id\":\"remove_extra_spaces\",\"enabled\":true},{\"id\":\"remove_urls_emails\",\"enabled\":true}],\"segmentation\":{\"separator\":\"###\",\"max_tokens\":500}},\"mode\":\"custom\"}}";

        // 构建 multipart 请求体（阻塞）
        MultiValueMap<String, HttpEntity<?>> multipartData;
        try {
            multipartData = MultipartBodyBuilderBuilder(jsonData, file);
            multipartData.forEach((k, v) -> log.info("part: {}, entity: {}", k, v));

        } catch (IOException e) {
            log.error("文件错误", e);
            throw new RuntimeException(e);
        }

        // 发起阻塞请求
        JsonNode responseJson = client.post()
                .uri(uri)
                .header(HttpHeaders.AUTHORIZATION, "Bearer " + config.getDatasetsApiKey())
                .contentType(MediaType.MULTIPART_FORM_DATA)
                .bodyValue(multipartData)
                .retrieve()
                .onStatus(HttpStatus::isError, r -> r.bodyToMono(String.class).flatMap(msg -> {
                    log.error("上传失败：状态码={}, 响应体={}", r.statusCode(), msg);
                    return Mono.error(new RuntimeException("上传失败: " + msg));
                }))
                .bodyToMono(JsonNode.class)
                .timeout(Duration.ofSeconds(10))
                .block();
        // 提取结果
        JsonNode document = responseJson.get("document");
        if (document == null) {
            throw new RuntimeException("上传失败：未返回 document 字段");
        }

        String docId = document.get("id").asText();
        String docName = document.get("name").asText();

        // 保存文档记录
        LlmDatasetsTpml tpml = new LlmDatasetsTpml();
        tpml.setDocId(docId);
        tpml.setDocName(docName);
        tpml.setDatasetsId(llmDataset.getDatasetsId());
        tpml.setDocType(1);
        tpml.setDocStoType(2);
        llmDatasetsTpmlRepository.save(tpml);

        return JSON.toJSONString(responseJson);
    }


    private MultiValueMap<String, HttpEntity<?>> MultipartBodyBuilderBuilder(String jsonData, MultipartFile file) throws IOException {
        if (file == null || file.isEmpty()) {
            throw new IllegalArgumentException("上传文件为空！");
        }

        MultipartBodyBuilder builder = new MultipartBodyBuilder();

        // data 字段 - 明确使用 HttpEntity 防止 charset=utf-8 被追加
        HttpHeaders dataHeaders = new HttpHeaders();
        dataHeaders.setContentType(MediaType.TEXT_PLAIN);
        HttpEntity<String> jsonEntity = new HttpEntity<>(jsonData, dataHeaders);
        builder.part("data", jsonEntity); // ✅字段名必须为 data

        // file 字段 - 显式设置 Content-Disposition 避免 Spring 自动替换
        HttpHeaders fileHeaders = new HttpHeaders();
        fileHeaders.setContentDispositionFormData("file", file.getOriginalFilename());
        HttpEntity<Resource> fileEntity = new HttpEntity<>(file.getResource(), fileHeaders);
        builder.part("file", fileEntity);

        return builder.build();
    }




    @Override
    public Flux<EventData> buildChatApi(LlmModelConfigApi config, CPReqBody query) {
        // 1. 异步获取 LlmModelConfig 和 LlmApp
        Mono<LlmModelConfig> configMono = Mono.fromCallable(() ->
                llmModelConfigRepository.findById(config.getPlatformId())
                        .orElseThrow(() -> new RuntimeException("平台ID不存在!"))
        ).subscribeOn(Schedulers.boundedElastic());

        Mono<LlmApp> appMono = Mono.fromCallable(() ->
                llmAppRepository.findFirstByAppId(query.getLevelId_2())
                        .orElseThrow(() -> new RuntimeException("平台ID不存在!"))
        ).subscribeOn(Schedulers.boundedElastic());

        // 2. 组合两个 Mono，全部放到 flatMap/flatMapMany 内
        return Mono.zip(configMono, appMono).flatMapMany(tuple -> {
            WebClient webClient = buildApi(tuple.getT1(), tuple.getT2());
            QaQuestionQuery body = JSON.parseObject(query.getBody().toJSONString(), QaQuestionQuery.class);
            body.setAppId(query.getLevelId_2());
            body.setUserId(query.getUserId());
            Map<String, Object> param = body.builderParamBody();
            param.put("user", String.valueOf(query.getUserId()));
            return webClient.post()
                    .uri(config.getApiAdd())
                    .contentType(MediaType.APPLICATION_JSON)
                    .accept(MediaType.APPLICATION_JSON)
                    .bodyValue(param)
                    .retrieve()
                    .bodyToFlux(String.class)
                    .scan(EventData.builder().busData(body).requestTimestamp(System.currentTimeMillis()).build(),
                            (eventData, chunk) -> {
                                Map<String, Object> map = JSON.parseObject(chunk, Map.class);
                                for (EventHandler handler : getHandlers()) {
                                    if (handler.supports(map)) {
                                        eventData = handler.handle(map, eventData);
                                    }
                                }
                                return eventData;
                            });
        });
    }


}
